﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;

namespace eProject.Model
{
    public class Activity : INotifyPropertyChanged
    {

        #region Constants
        public const int MaxPriority = 10;
        public const int MinPriority = 1;
        #endregion

        #region Constructor
        private Activity(Task task, Employee employee, Period period, int priority)
        {
            Task = task;
            Employee = employee;
            _period = period;
            _priority = priority;//TODO checking parameters
        }
        #endregion

        #region Events

        public event PeriodChangedEventHandler PeriodChanged;

        public event ActivityChangedEventHandler PriorityChanged;

        public event ActivityChangedEventHandler ManHoursChanged;

        public event PropertyChangedEventHandler PropertyChanged;

        public delegate void PeriodChangedEventHandler(Activity activity, Period oldValue, Period newValue);

        public delegate void ActivityChangedEventHandler(Activity activity);

        private void InvokePeriodChanged(Period oldValue, Period newValue)
        {
            var changed = PeriodChanged;
            if (changed != null) changed(this, oldValue, newValue);
            InvokePropertyChanged("Period");
        }

        private void InvokePriorityChanged()
        {
            var changed = PriorityChanged;
            if (changed != null) changed(this);
            InvokePropertyChanged("Priority");
        }

        private void InvokeManHoursChanged(Activity activity)
        {
            var changed = ManHoursChanged;
            if (changed != null) changed(activity);
            InvokePropertyChanged("ManHours");
        }        

        private void InvokePropertyChanged(string propertyName)
        {
            var changed = PropertyChanged;
            if (changed != null) changed(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion

        #region Properties

        public Task Task{get; private set;}

        public  Employee Employee{ get; private set;}
        private Period _period;
        public Period Period 
        {
            get{return _period;}
            set
            {
                if (_period == value) return;

                var oldValue = _period;
                _period = value;
                InvokePeriodChanged(oldValue,_period);
            }}

        private int _priority;
        public int Priority
        {
            get { return _priority;}
            set
            {
                if (value > MaxPriority || value < MinPriority)
                {
                    throw new ArgumentException("Value of Priority must be between " + MinPriority + " and " + MaxPriority);
                }
                if (_priority == value) return;
                _priority = value;  
                InvokePriorityChanged();
            }
        }

        public double ManHours
        {
            get { return Manager.GetManHours(this); }
        }

        #endregion
        
        #region StaticPart
        private static readonly ActivitiesManager Manager = new ActivitiesManager();        

        internal static Activity CreateActivity(Task task, Employee employee, Period period, int priority)
        {
            var act = new Activity(task, employee, period, priority);
            Manager.AddActivity(act);            
            return act;
        }

        internal static void DeleteActivity(Activity activity)
        {
            Manager.RemoveActivity(activity);
        }

        public static IEnumerable<Activity> GetActivities()
        {
            return Manager.GetActivities();
        }

        public static IEnumerable<Activity> GetActivities(Task task)
        {
            return Manager.GetActivities(task);
        }
        //TODO write GetActivities by Employee and other, if needed

        #endregion       

        #region ActivitiesManager
        private class ActivitiesManager
        {
            private readonly Dictionary<Activity, double> _activities = new Dictionary<Activity, double>();

            public long Count { get { return _activities.Count; } }
       

            private static void NotifyManHoursChanged(IEnumerable<Activity> activities)
            {
                foreach (var activity in activities)
                {
                    activity.InvokeManHoursChanged(activity);
                }
            }

            //TODO ^^^

            public void AddActivity(Activity activity)
            {
                _activities.Add(activity, 0);
                activity.PeriodChanged += OnActivityPeriodChanged;
                activity.PriorityChanged += OnActivityPriorityChanged;
                RecalculateByActivity(activity);
            }

            public void RemoveActivity(Activity activity)
            {
                _activities.Remove(activity);
                activity.PeriodChanged -= OnActivityPeriodChanged;
                activity.PriorityChanged -= OnActivityPriorityChanged;
                RecalculateByActivity(activity);
            }

            public IEnumerable<Activity> GetActivities(Employee employee)
            {
                var activities = from act in _activities.Keys
                                 where act.Employee == employee
                                 select act;
                return activities.ToList();
            }

            public IEnumerable<Activity> GetActivities(Task task)
            {
                var activities = from act in _activities.Keys
                                 where act.Task == task
                                 select act;
                return activities.ToList();
            }

            public IEnumerable<Activity> GetActivities()
            {
                return _activities.Keys;//TODO checkit
            }

            public IEnumerable<Activity> GetActivities(Task task, Period period)
            {
                return GetByPeriod(GetActivities(task), period);
            }

            public IEnumerable<Activity> GetActivities(Employee employee, Period period)
            {
                return GetByPeriod(GetActivities(employee), period);
            }

            public double GetManHours(Activity activity)
            {
                return _activities[activity];//TODO Cheking if if _activities doesn't contain the activity
            }

            private static IEnumerable<Activity> GetByPeriod(IEnumerable<Activity> activities, Period period)
            {
                var result = from act in activities
                             where act.Period.Begin < period.End && act.Period.End > period.Begin
                             select act;
                return result.ToList();
            }

            private void OnActivityPeriodChanged(Activity activity, Period oldValue, Period newValue)
            {
                var begin = oldValue.Begin < newValue.Begin ? oldValue.Begin : newValue.Begin;
                var end = oldValue.End > newValue.End ? oldValue.End : newValue.End;
                var period = new Period(begin, end);
                RecalculateByEmployeeAndPeriod(activity.Employee, period);
            }

            private void OnActivityPriorityChanged(Activity activity)
            {
                RecalculateByActivity(activity);
            }

            private static Period CalculatePeriod(IEnumerable<Activity> activities, Period period)
            {
                var acts = from act in activities
                           where period.Begin < act.Period.End && period.Begin > act.Period.Begin
                           select act;
                if (acts.Count() > 0)
                {
                    var newBegin = acts.Min(a => a.Period.Begin);
                    return CalculatePeriod(activities, new Period(newBegin, period.End));
                }

                acts = from act in activities
                       where period.End > act.Period.Begin && period.End < act.Period.End
                       select act;
                if (acts.Count() > 0)
                {
                    var newEnd = acts.Max(a => a.Period.End);
                    return CalculatePeriod(activities, new Period(period.Begin, newEnd));
                }
                return period;
            }

            private static IList<Period> GetIntersections(IEnumerable<Activity> activities)
            {
                var intscs = (from act in activities
                              select act.Period.Begin).Union(from act in activities
                                                             select act.Period.End).Distinct().OrderBy(d => d.Ticks).ToList();
                var periods = new List<Period>(intscs.Count - 1);
                for (var i = 0; i < intscs.Count - 1; i++)
                {
                    var p = new Period(intscs[i], intscs[i + 1]);
                    periods.Add(p);
                }
                return periods;
            }

            private void RecalculateActivities(IEnumerable<Activity> activities)
            {
                if (activities.Count() == 0)
                {
                    return;
                }
                foreach (var activity in activities)
                {
                    _activities[activity] = 0;
                }

                var intersections = GetIntersections(activities);

                foreach (var period in intersections)
                {
                    var acts = GetByPeriod(activities, period);
                    var sum = acts.Sum(a => a.Priority);

                    foreach (var act in acts)
                    {
                        _activities[act] += (((double)act.Priority) / sum) * act.Employee.GetManHours(period);//TODO replace this 
                    }
                }
                NotifyManHoursChanged(activities);
            }

            private void RecalculateByActivity(Activity activity)
            {
                RecalculateByEmployeeAndPeriod(activity.Employee, activity.Period);
            }

            private void RecalculateByEmployeeAndPeriod(Employee employee, Period period)
            {
                var acts = GetActivities(employee);
                var p = CalculatePeriod(acts, period);
                var result = GetByPeriod(acts, p);
                RecalculateActivities(result);
            }

        }
        #endregion

       
    }
}
